package com.cjl.android.asynctask;


import org.jetbrains.annotations.NotNull;

import java.util.ArrayDeque;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public abstract class AsyncTask<Params,Process,Result> {

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<>(10);

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread(@NotNull Runnable r) {
            return new Thread(r,"cjl_asyncTask #"+mCount.getAndIncrement());
        }
    };

    private static volatile Executor sDefaultExecutor = new SerialExecutor();

    public static final Executor THREAD_POOL_EXECUTOR =
            new ThreadPoolExecutor(5,128, 1,TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);

    private final WorkerRunnable<Params,Result> mWorker;
    private final FutureTask<Result> mFuture;
    private final AtomicBoolean mCancelled = new AtomicBoolean();
    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    protected AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            @Override
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                return postResult(doInBackground(mParams));
            }
        };

        mFuture = new FutureTask<Result>(mWorker){
            @Override
            protected void done() {
                System.out.println("AsyncTask.done");
                try {
                    postResultIfNotInvoked(get());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }catch (ExecutionException e){
                    throw new RuntimeException("an error occured while executing doInBackground");
                }catch (CancellationException e){
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvokked = mTaskInvoked.get();
        if (!wasTaskInvokked)
            postResult(result);
    }

    private Result postResult(Result result){
        finish(result);
        return result;
    }


    private void finish(Result result) {
        if (mCancelled.get()){
            onCancelled(result);
        }else {
            onPostExecute(result);
        }
    }

    public final boolean cancel(boolean mayInterruptIfRunning){
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }

    public static void execute(Runnable runnable){
        sDefaultExecutor.execute(runnable);
    }

    public final AsyncTask<Params,Process,Result> execute(Params... params){
        return executeOnExecutor(sDefaultExecutor,params);
    }

    public final AsyncTask<Params,Process,Result> executeOnExecutor(Executor executor, Params[] params) {
        onPreExecute();
        mWorker.mParams = params;
        executor.execute(mFuture);
        return this;
    }

    protected abstract Result doInBackground(Params...  params);

    //TODO ??? where run
    //runs before doInBackground
    protected void onPreExecute(){}

    //runs after doInBackground
    protected void onPostExecute(Result result){}


    protected void onCancelled(Result result) {}

    protected void onProgressUpdate(Process... processes){}

    public final void publishProgress(Process... values){
        if (!mCancelled.get()){
            onProgressUpdate(values);
        }
    }

    public final void log(String msg){
        System.out.println(Thread.currentThread().getName()+"AsyncTask.log "+msg);
    }

    protected static abstract class WorkerRunnable<XParams,YResult>
        implements Callable<YResult>{
        XParams[] mParams;
    }

    private static class SerialExecutor implements Executor{
        ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
        Runnable mActive;

        @Override
        public void execute(Runnable command) {
            mTasks.offer(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("SerialExecutor.run before command run");
                        command.run();
                    }finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null){
                scheduleNext();
            }
        }

        private void scheduleNext() {
            if ((mActive = mTasks.poll()) != null){
                System.out.println("SerialExecutor.scheduleNext tpx.execute");
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }
}
